Implementing a Go Module for SWUpdate: Enhancing Embedded Systems with Go
Introduction #
Embedded systems are becoming increasingly sophisticated, and ensuring they are up-to-date is critical for security and functionality. SWUpdate is a popular open-source tool designed to manage software updates on embedded systems. Traditionally, modules for SWUpdate have been implemented in C++ or Python. However, I decided to create a Go module for SWUpdate. This blog post explores the reasoning behind this decision, the challenges faced during implementation, and the benefits of using Go over the more common C++ or Python implementations.
Background Context #
At ifm, we have been working closely with Stefano Babic, the maintainer and creative head behind SWUpdate, for several years. The camera division of ifm was an early adopter of the SWUpdate framework and have integrated it extensively into their embedded Linux camera products. If you are interested in ifm products and want to learn more, please visit ifm’s website.
Why SWUpdate? #
SWUpdate is a robust and Open Source tool that simplifies the process of updating embedded systems. It supports various update mechanisms, including local storage, network downloads, and more. Its flexibility and reliability make it a go-to choice for many developers working on embedded systems. However, the choice of programming language for interfacing SWUpdate can significantly impact the ease of development, performance, and maintainability.
Why Go? #
Go offers several compelling advantages for developing modules for embedded systems:
- Batteries Included: Go comes with a comprehensive standard library that provides many essential packages out-of-the-box, reducing the need for third-party dependencies.
- Simplified Build Process: Unlike C++ or Java, which require build tools like CMake or Maven, Go’s toolchain is straightforward. Building and compiling code is as simple as running
go build
. - Cross-Platform Compilation: Go provides a simple way to compile code for all major platforms, including Linux, macOS, and Windows, regardless of the operating system on which your toolchain runs.
- Concurrency Model: Go’s concurrency model, based on goroutines and channels, makes it easy to manage concurrent tasks, which is crucial for handling updates efficiently.
- Memory Safety: Go’s garbage-collected memory management reduces the risk of memory leaks and other related issues, contributing to more robust and secure applications.
Challenges in Implementation #
While implementing the software update client in Go, I encountered several challenges that required careful debugging and problem-solving. My journey began with understanding how uploading to SWUpdate works in general, using tools like curl.
Uploading a File with curl
#
To illustrate the upload process using curl
, here’s an example command that uploads a file to a specific URL. Suppose you have a file named OVP81x_Firmware_v1.4.30.swu
and you want to upload it to http://192.168.0.69:8080/upload
with the form field name file
.
curl -F "file=@Firmware.swu" http://192.168.0.69:8080/upload
Replace /path/to/OVP81x_Firmware_v1.4.30.swu with the actual path to your file. For example, if your file is located at /home/user/documents/OVP81x_Firmware_v1.4.30.swu, the command would be:
curl -F "file=@/tmp/OVP81x_Firmware_v1.4.30.swu" \
http://192.168.0.69:8080/upload
This command will upload OVP81x_Firmware_v1.4.30.swu to the server at http://192.168.0.69:8080/upload with the form field name file. Using curl, I was able to successfully upload files to the SWUpdate backend without issues.
Multipart Form Upload Issues in Go #
In our use-case a Webserver handles the server-side aspects of of the SWUpdate application. For the specific product, the decision was made to use the meta-swupdate Yocto layer more or less in its default configuration, which includes the Mongoose Webserver to handle the server-side aspects of the update process. However, I faced a perplexing problem: multipart form uploads did not support chunked uploads in my stack. This issue manifested as failures in the Go implementation, despite using the same form upload approach that worked flawlessly with curl.
The decision to use Mongoose and meta-swupdate was made a while ago. For compatibility with any existing firmware in the field, this configuration cannot be changed.
One of the particular challenges was that for the specific device, an OVP810, the update file size is bigger than 1.2 GB. It is highly inefficient to store such a large file in memory before uploading. The solution to this issue typically involves using an io.Pipe to connect the io.Reader interface of the file to the io.Writer interface of the multipart form uploader. However, this approach expects the receiver to accept chunked uploads, which was not working in my case.
To mitigate this issue, the complete transfer size needs to be included in the request. To achieve this, I decided to use the Go module github.com/technoweenie/multipartstreamer, which seemed reasonably well-designed for this purpose.
Debugging the Issue #
Debugging this issue was a complex task. The primary challenge was to determine where the problem originated—whether it was in the Go client, the Mongoose Webserver, or the SWUpdate backend. I had to systematically isolate each component to identify the root cause.
Solution Using multipartstreamer
The multipartstreamer module allowed me to include the complete transfer size, including the HTTP headers, ensuring compatibility with the server. Here’s how I used it to solve the problem:
package main
import (
"github.com/technoweenie/multipartstreamer"
"net/http"
"os"
"path/filepath"
)
func uploadFile(url string, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
ms := multipartstreamer.New()
ms.WriteFile("file", filepath.Base(filePath), file)
request, err := http.NewRequest("POST", url, ms.GetReader())
if err != nil {
return err
}
request.Header.Set("Content-Type", ms.GetContentType())
request.ContentLength = ms.Len()
client := &http.Client{}
resp, err := client.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to upload file, status code: %d", resp.StatusCode)
}
return nil
}
func main() {
err := uploadFile("http://192.168.0.69:8080/upload", "/path/to/OVP81x_Firmware_v1.4.30.swu")
if err != nil {
panic(err)
}
}
Possible Next Steps #
One possible next step is to directly load an update from a service like Sonatype Nexus and redirect it to the SWUpdate. If the cloud storage provides information about the actual file size, this should be possible without any large in-memory buffering or the need to download and reupload the file.
Conclusion #
Choosing Go for implementing a SWUpdate module brings several benefits, including simplicity, performance, and safety. However, like any development effort, it comes with its challenges. My experience with multipart form upload issues highlighted the importance of thorough debugging and understanding the nuances of each component in the stack.
By leveraging Go and using the multipartstreamer module, I created a robust, efficient, and maintainable module for managing software updates in embedded environments. Despite the challenges, the advantages of using Go far outweighed the difficulties, making it a worthy choice for embedded systems development.
Call to Action #
If you’re working on embedded systems and are considering interfacing SWUpdate, give Go a try. Its balance of simplicity and performance might be exactly what you need to take your project to the next level. If you need any consulting or have questions, please do not hesitate to reach out. The final update code for the OVP8xx can be found on GitHub.
Happy coding!